// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.Threading.Actors;
using System.Threading.Tasks;
using ActorFramework.Fakebook;

namespace PersonActor
{
    /*
     * A Person actor represents a user name, their profile information, and a picture for themselves.
     * Incoming friendship requests are stored here too.
     * 
     * PersonActor
     *     First Name, Last Name
     *     Profile Info
     *         Gender
     *         City
     *         Birthday
     *         Relationship Status
     *         Picture
     *         Eventually, hobbies, favorite movies & TV shows, etc.
     *     Incoming friendship requests (stored as a List<String> serialized in this actor)
     *     
     * Provides several events:
     *     NewFriendRequest
     *     FriendRequestAccepted
     *     FriendRequestRejected
     * 
     * Rest of Fakebook relies on other actors:
     *     Friends List - a separate CloudList<String>
     *     Photos - a CloudStringDictionary<PictureInfo>
     *     News Posts - a CloudList<String>
     *     NewsFeed - an actor that aggregates your friends' (and your) news posts.  Also subscribes to 
     *         your Friends list so it knows when to subscribe or unsubscribe to a new friend's news posts.
     */
    public static class PersonActor
    {
        private const int NumPartitions = 10;

        private const String NewFriendRequestEventName = "NewFriendRequest";
        private const String FriendRequestProcessedEventName = "FriendRequestProcessed";

        // A global Fakebook list containing all user names.
        private const String AllUsersListName = "fabric:/actor/list/AllFakebookUsers";

        // These services are partitioned based on the user name.
        private const String FriendsListName = "fabric:/actor/list/Friends";
        private const String PeopleActorName = "fabric:/actor/fakebook/People";
        private const String NewsPostsListName = "fabric:/actor/list/NewsPosts";
        private const String NewsFeedName = "fabric:/actor/fakebook/Newsfeed";
        private const String PhotosDictionaryName = "fabric:/actor/dictionary/Photos";

        private const String ListCreationString = "applicationType=fabric:/actor/list;serviceType=ListActorReplica";
        private const String DictionaryCreationString = "applicationType=fabric:/actor/dictionary;serviceType=DictionaryActorReplica";
        private const String NewsFeedCreationString = "applicationType=fabric:/actor/fakebook;serviceType=EmptyActorReplica";

        // Account name could be an arbitrary map, not just firstName.lastName
        private static String Account(String firstName, String lastName)
        {
            return firstName + "." + lastName;
        }

        private static String FirstNameKey(String accountName)
        {
            return accountName + "$FirstName";
        }

        private static String LastNameKey(String accountName)
        {
            return accountName + "$LastName";
        }

        private static String ProfileKey(String accountName, String profileItem)
        {
            return accountName + "$" + profileItem;
        }

        private static String FriendRequestsKey(String accountName)
        {
            return accountName + "$FriendRequests";
        }

        [ActorMethod]
        public static Object TryConstruct(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 1);

            String accountName = (String)parameters[0];

            Object obj;
            bool exists = state.TryGet(FirstNameKey(accountName), out obj);

            // In case something went wrong, we may have lost one of these other collections.  (Often due to restarting the cluster.)
            // The service instance may exist, but we may not have run the right constructors on data structures like lists yet.
            // So, don't skip the rest of this initialization.

            // Add this account name to a global list of all accounts, if it's not already there.
            IActorProxy allUsers = state.GetActorProxy(AllUsersListName);
            allUsers.Command("AddOnce", new Object[] { "AllFakebookUsers", accountName });

            // Must create actor instances for:
            //    Friends (CloudList<String>)
            //    NewsPosts (CloudList<String>, though could become CloudList<Message>)
            //    NewsFeedActor
            //    Photos (CloudStringDictionary<PictureInfo>)
            
            // Create Friends list
            state.CreateActor(FriendsListName, ListCreationString, NumPartitions);
            IActorProxy friends = state.GetActorProxy(FriendsListName, accountName);
            friends.Command("TryConstruct", new Object[] { "Friends" });  // @TODO: pass a type constraint here.
            // We cache IActorProxies, so don't dispose of them.  

            // Create NewsPosts list
            state.CreateActor(NewsPostsListName, ListCreationString, NumPartitions);
            IActorProxy newsPosts = state.GetActorProxy(NewsPostsListName, accountName);
            newsPosts.Command("TryConstruct", new Object[] { "NewsPosts" });  // @TODO: pass a type constraint here.  String or Message?

            // @TODO: Hold off on NewsFeed for now.  Let the client create that, so it can upload the newsfeed actor's assembly to the cloud.
            //String newsFeedActorName = String.Format(NewsFeedFormatString, accountName);

            // Create Photos Dictionary (String -> PictureInfo)
            state.CreateActor(PhotosDictionaryName, DictionaryCreationString, NumPartitions);
            IActorProxy photos = state.GetActorProxy(PhotosDictionaryName, accountName);
            photos.Command("TryConstruct", new Object[] { "Photos", typeof(String).FullName, typeof(PictureInfo).FullName });                

            return exists;
        }

        // Retrieves profile information from the actor's state, using a unique name.
        [ActorMethod]
        public static Object GetProfileInfo(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 2);

            String accountName = (String)parameters[0];
            String profileItem = (String)parameters[1];

            String profileKey = ProfileKey(accountName, profileItem);
            Object profileData = null;
            state.TryGet(profileKey, out profileData);

            return profileData;
        }

        // Stores profile information directly in the actor's state with a unique name.
        [ActorMethod]
        public static Object SetProfileInfo(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 3);

            String accountName = (String)parameters[0];
            String profileItem = (String)parameters[1];
            Object profileData = parameters[2];

            String profileKey = ProfileKey(accountName, profileItem);
            state.Set(profileKey, profileData);
            state.Publish("ProfileChange", accountName, profileItem, profileData);

            return null;
        }

        #region Friend Requests & Maintenance

        [ActorMethod]
        public static Object AcceptFriendRequest(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 2);

            String accountName = (String)parameters[0];
            String friendAccountName = (String)parameters[1];

            // Add to friends lists
            AddToFriendsListsWithPromises(state, accountName, new String[] { friendAccountName });

            // Remove from list of pending friend requests
            RemoveFriendRequest(state, accountName, friendAccountName);

            // Call the friend's actor who initiated the friend request, accepting them.
            IActorProxy requestingFriend = state.GetActorProxy(PeopleActorName, friendAccountName);
            requestingFriend.Command("FriendRequestProcessed", new Object[] { accountName, true, "I will be your friend." });

            return null;
        }

        private static void AddToFriendsLists(IActorState state, String accountName, String[] friendAccountNames)
        {
            IActorProxy myFriends = state.GetActorProxy(FriendsListName, accountName);
            String myFriendsListName = "Friends";

            Object[] lumpedParams = new Object[1 + friendAccountNames.Length];
            lumpedParams[0] = myFriendsListName;
            Array.Copy(friendAccountNames, 0, lumpedParams, 1, friendAccountNames.Length);
#if BLOCK_TIL_DONE
            IActorPromise myFriendsUpdate = myFriends.Request("AddOnce", lumpedParams);
#else
            myFriends.Command("AddOnce", lumpedParams);
#endif

            var otherFriendsUpdates = new List<IActorPromise>(friendAccountNames.Length);
            foreach (String friendAccountName in friendAccountNames)
            {
                IActorProxy theirFriends = state.GetActorProxy(FriendsListName, friendAccountName);
                String theirFriendsListName = "Friends";
#if BLOCK_TIL_DONE
                var promise = theirFriends.Request("AddOnce", new Object[] { theirFriendsListName, accountName });
                otherFriendsUpdates.Add(promise);
#else
                theirFriends.Command("AddOnce", new Object[] { theirFriendsListName, accountName });
#endif
            }

#if BLOCK_TIL_DONE
            // Block until complete - our client test case kinda depends on it.
            myFriendsUpdate.Wait();
            foreach (var promise in otherFriendsUpdates)
                promise.Wait();
#endif
        }

        [ActorMethod]
        public static Object AddToMyFriendsContinuation(IActorState state, Object[] parameters)
        {
            var proxyPromise = (IActorPromise)parameters[0];
            var myFriends = proxyPromise.GetTypedResult<IActorProxy>();

            Object[] lumpedParams = parameters.Skip(1).ToArray();
#if BLOCK_TIL_DONE
            IActorPromise myFriendsUpdate = myFriends.Request("AddOnce", lumpedParams);
#else
            myFriends.Command("AddOnce", lumpedParams);
#endif
            return null;
        }

        [ActorMethod]
        public static Object AddToTheirFriendsListContinuation(IActorState state, Object[] parameters)
        {
            var proxyPromise = (IActorPromise)parameters[0];
            var theirFriends = proxyPromise.GetTypedResult<IActorProxy>();

            String accountName = (String)parameters[1];
#if BLOCK_TIL_DONE
            var promise = theirFriends.Request("AddOnce", new Object[] { theirFriendsListName, accountName });
            otherFriendsUpdates.Add(promise);
            promise.Wait();
#else
            theirFriends.Command("AddOnce", new Object[] { "Friends", accountName });
#endif
            return null;
        }

        private static void AddToFriendsListsWithPromises(IActorState state, String accountName, String[] friendAccountNames)
        {
            IActorPromise myFriends = state.GetActorProxyAsync(FriendsListName, accountName);
            String myFriendsListName = "Friends";

            Object[] lumpedParams = new Object[1 + friendAccountNames.Length];
            lumpedParams[0] = myFriendsListName;
            Array.Copy(friendAccountNames, 0, lumpedParams, 1, friendAccountNames.Length);

            IActorPromise myFriendsUpdate = myFriends.ContinueWith("AddToMyFriendsContinuation", lumpedParams);

            var otherFriendsUpdates = new List<IActorPromise>(friendAccountNames.Length);
            foreach (String friendAccountName in friendAccountNames)
            {
                IActorPromise theirFriends = state.GetActorProxyAsync(FriendsListName, friendAccountName);
                var continuationPromise = theirFriends.ContinueWith("AddToTheirFriendsListContinuation", new Object[] { accountName });
#if BLOCK_TIL_DONE
                otherFriendsUpdates.Add(continuationPromise);
#endif
            }

#if BLOCK_TIL_DONE
            // Block until complete - our client test case kinda depends on it.
            myFriendsUpdate.Wait();
            foreach (var continuationPromise in otherFriendsUpdates)
                continuationPromise.Wait();
#endif
        }


        [ActorMethod]
        public static Object ForceMakeFriends(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length >= 2);

            String accountName = (String)parameters[0];
            String[] friendAccountNames = new String[parameters.Length - 1];
            Array.Copy(parameters, 1, friendAccountNames, 0, friendAccountNames.Length);

            AddToFriendsListsWithPromises(state, accountName, friendAccountNames);

            return null;
        }

        [ActorMethod]
        public static Object RejectFriendRequest(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 3);

            String accountName = (String)parameters[0];
            String friendAccountName = (String)parameters[1];
            String message = (String) parameters[2];

            // Remove from list of pending friend requests
            RemoveFriendRequest(state, accountName, friendAccountName);

            // Call the friend's actor who initiated the friend request, rejecting them.
            IActorProxy requestingFriend = state.GetActorProxy(PeopleActorName, friendAccountName);
            requestingFriend.Command("FriendRequestProcessed", new Object[] { accountName, false, message });

            return null;
        }

        // Parameters:
        //   Friend who accepted/rejected your request
        //   Accepted?  (boolean)
        //   Message
        [ActorMethod]
        public static Object FriendRequestProcessed(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 3);

            // @TODO: The separation between event payload and payload qualifiers seems a bit goofy.
            state.Publish(FriendRequestProcessedEventName, parameters[0], parameters[1], parameters[2]);
            return null;
        }


        // Adds a friend request to the list of outstanding friend requests, then publishes 
        // a new friend request event to the friend-ee.
        [ActorMethod]
        public static Object MakeFriendRequest(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 2);

            String accountName = (String)parameters[0];
            String askingAccountName = (String)parameters[1];

            String friendRequestsKey = FriendRequestsKey(accountName);
            List<String> friendRequests = null;
            Object obj;
            if (state.TryGet(friendRequestsKey, out obj))
                friendRequests = (List<String>)obj;
            if (friendRequests == null)
                friendRequests = new List<String>();
            friendRequests.Add(askingAccountName);
            state.Set(friendRequestsKey, friendRequests);

            state.Publish(NewFriendRequestEventName, askingAccountName);

            return null;
        }

        private static void RemoveFriendRequest(IActorState state, String accountName, String friendAccountName)
        {
            String friendRequestsKey = FriendRequestsKey(accountName);
            Object obj = null;
            if (state.TryGet(friendRequestsKey, out obj))
            {
                List<String> friendRequests = (List<String>)obj;
                friendRequests.Remove(friendAccountName);
                if (friendRequests.Count == 0)
                    friendRequests = null;
                state.Set(friendRequestsKey, friendRequests);
            }
        }

        [ActorMethod]
        public static Object Unfriend(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 2);

            String accountName = (String)parameters[0];
            String exFriendAccountName = (String)parameters[1];

            // Remove from both friends lists.
            IActorProxy myFriends = state.GetActorProxy(FriendsListName, accountName);
            String myFriendsListName = "Friends";
            IActorPromise result1 = myFriends.Request("Remove", new Object[] { myFriendsListName, 1, exFriendAccountName });

            IActorProxy theirFriends = state.GetActorProxy(FriendsListName, exFriendAccountName);
            String theirFriendsListName = "Friends";
            IActorPromise result2 = theirFriends.Request("Remove", new Object[] { theirFriendsListName, 1, accountName });

            // Wait for both friends requests to get processed, so NewsFeed doesn't show duplicate posts.
            Object junk = result1.Result;
            // @TODO: Would like to use .ContinueWith(Action), or .Wait(IActorPromise[]).  However, actor methods cannot
            // be async methods.  They can return a Task<T>, but the lock on the IActorState will be released and not be reaquired.
            junk = result2.Result;

            // Post to both people's news feeds.
            IActorProxy myNewsPosts = state.GetActorProxy(NewsPostsListName, accountName);
            String unfriendMessage = String.Format("{0} unfriended {1}", accountName, exFriendAccountName);
            myNewsPosts.Command("Push", new Object[] { "NewsPosts", "right", unfriendMessage });

            IActorProxy theirNewsPosts = state.GetActorProxy(NewsPostsListName, exFriendAccountName);
            theirNewsPosts.Command("Push", new Object[] { "NewsPosts", "right", unfriendMessage });

            return null;
        }

        #endregion Friend Requests & Maintenance

    }
}
